using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using Needle.Core.Reflection;

namespace Needle.Core.Factory
{
    /// <summary>
    /// Creates a dynamic method for constructing a new instance.
    /// </summary>
    public class Constructor
    {
        /// <summary>
        /// Creation delegate
        /// </summary>
        public delegate object InstanceCreator(params object[] args);

        private static readonly MethodSignature _InstanceCreatorSignature;

        /// <summary>
        /// </summary>
        static Constructor()
        {
            _InstanceCreatorSignature = new MethodSignature(typeof(object), typeof(object[]));
        }

        private ConstructorInfo _Info;
        /// <summary>
        /// Constructor info
        /// </summary>
        public ConstructorInfo Info
        {
            get { return _Info; }
        }
        /// <summary>
        /// delgate method to create a new instance
        /// </summary>
        public readonly InstanceCreator Invoke;

        /// <summary>
        /// Initializes a new instance of the Constructor class.
        /// </summary>
        /// <param name="info"></param>
        public Constructor(ConstructorInfo info)
        {
            _Info = info;
            List<Type> paramTypes = new List<Type>();
            paramTypes.Add(this.GetType());
            paramTypes.AddRange(_InstanceCreatorSignature.ParamTypes);
            DynamicMethod method = new DynamicMethod(
                                       "Invoke",
                                       _InstanceCreatorSignature.ReturnType,
                                       paramTypes.ToArray(),
                                       this.GetType()
                                       );
            GenerateIL(method);
            Invoke = (InstanceCreator)method.CreateDelegate(typeof(InstanceCreator), this);
        }

        private void GenerateIL(DynamicMethod method)
        {
            ILGenerator generator = method.GetILGenerator();
            ParameterInfo[] parameters = _Info.GetParameters();
            LocalBuilder argCount = generator.DeclareLocal(typeof(int));

            Label notNull = generator.DefineLabel();
            Label checkCount = generator.DefineLabel();
            Label argCountOk = generator.DefineLabel();

            /* IF the argument array is null... */
            generator.Emit(OpCodes.Ldarg_1);
            generator.Emit(OpCodes.Ldnull);
            generator.Emit(OpCodes.Call, typeof(Object).GetMethod("ReferenceEquals", BindingFlags.Static | BindingFlags.Public));
            generator.Emit(OpCodes.Brfalse_S, notNull);

            /* THEN set argCount to 0 */
            generator.Emit(OpCodes.Ldc_I4_0);
            generator.Emit(OpCodes.Stloc, argCount);
            generator.Emit(OpCodes.Br_S, checkCount);

            /* ELSE set argCount to the argument array length */
            generator.MarkLabel(notNull);
            generator.Emit(OpCodes.Ldarg_1);
            generator.Emit(OpCodes.Call, typeof(object[]).GetProperty("Length").GetGetMethod());
            generator.Emit(OpCodes.Stloc, argCount);

            /* IF the argument count does not match... */
            generator.MarkLabel(checkCount);
            generator.Emit(OpCodes.Ldloc, argCount);
            generator.Emit(OpCodes.Ldc_I4, parameters.Length);
            generator.Emit(OpCodes.Beq_S, argCountOk);

            /* THEN throw an ArgumentException */
            generator.Emit(OpCodes.Ldstr, "Argument count mismatch");
            generator.Emit(OpCodes.Newobj, typeof(ArgumentException).GetConstructor(new Type[] { typeof(string) }));
            generator.Emit(OpCodes.Throw);

            /* ELSE call the constructor and return the new instance */
            generator.MarkLabel(argCountOk);
            foreach (ParameterInfo p in parameters)
            { /* load and cast the arguments */
                generator.Emit(OpCodes.Ldarg_1);
                generator.Emit(OpCodes.Ldc_I4, p.Position);
                generator.Emit(OpCodes.Ldelem_Ref);
                if (p.ParameterType.IsValueType)
                    generator.Emit(OpCodes.Unbox_Any, p.ParameterType);
                else
                    generator.Emit(OpCodes.Castclass, p.ParameterType);
            }

            generator.Emit(OpCodes.Newobj, _Info);
            generator.Emit(OpCodes.Ret);
        }

    }
}
